﻿namespace CacheProvider.Classic
{
    #region N A M E S P A C E   I M P O R T S

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Linq;
    using System.Web;
    using System.Web.Caching;
    using CacheProviderEntities;
    using CacheProviderInterfaces;
    using Microsoft.Practices.Composite.Presentation.Events;
    using System.Collections.Specialized;

    #endregion

    public class ClassicCacheProvider : CacheProvider
    {
        #region P R I V A T E   A T T R I B U T E S

        private string _name = "CacheProvider.ClassicCacheProvider";

        private string _description = "Cache Provider which wraps System.Web.Caching.Cache";

        private Cache _cacheService;

        private long _defaultExpirationTimeInMS;

        private HttpContext _httpContext;

        private IDependencyMonitoringManager _sqlMonitor;

        #endregion

        #region P R O P E R T I E S

        public override bool IsCacheTagSupported
        {
            get
            {
                return false;
            }
        }

        public override bool IsRegionSupported
        {
            get
            {
                return false;
            }
        }

        public override bool IsConcurrencySupported
        {
            get
            {
                return false;
            }
        }

        public override long DefaultExpirationTimeInMilliSeconds
        {
            get
            {
                return this._defaultExpirationTimeInMS;
            }
            set
            {
                this._defaultExpirationTimeInMS = value;
            }
        }

        public override bool IsNamedCacheSupported
        {
            get { return false; }
        }

        public override string CacheName
        {
            get { return string.Empty; }
        }

        public override short DataCacheFactoryPoolSize
        {
            get { return -1; }
        }

        public override bool EagerPoolingEnabled
        {
            get { return false; }
        }

        public override bool IsCacheItemVersioningSupported
        {
            get { return false; }
        }

        #endregion

        #region P R O V I D E R B A S E   M E M B E R   O V E R R I D E S

        public override string Name
        {
            get
            {
                return _name;
            }
        }

        public override string Description
        {
            get
            {
                return _description;
            }
        }

        public override void Initialize(string name,
            System.Collections.Specialized.NameValueCollection config)
        {
            // Initialize values from Web.config.
            if (null == config)
            {
                throw (new ArgumentNullException("config"));
            }
            if (string.IsNullOrEmpty(name))
            {
                name = this._name;
            }
            else
            {
                this._name = name;
            }

            if (string.IsNullOrEmpty(config["description"]))
            {
                config.Remove("description");
                config.Add("description", this._description);
            }
            else
            {
                this._description = config["description"];
            }

            // Call the base class implementation.
            base.Initialize(name, config);

            // Load configuration data.            
            this.DefaultExpirationTimeInMilliSeconds =
                Convert.ToInt64(config["defaultExpirationTimeInMS"] ?? "60000");

            if (HttpContext.Current != null && HttpContext.Current.Cache != null)
            {
                this._cacheService = HttpContext.Current.Cache;
            }
            else
            {
                var request = new HttpRequest("default.aspx", "http://srblrcaf01/tomcat/default.aspx", string.Empty);
                var writer = new StreamWriter(new MemoryStream());
                var response = new HttpResponse(writer);
                _httpContext = new HttpContext(request, response);

                if (_httpContext.Cache != null)
                {
                    this._cacheService = _httpContext.Cache;
                }
                else
                {
                    var cacheProviderErrorEventArgs = new CacheProviderErrorEventArgs()
                    {
                        Sender = this,
                        CacheKey = "N/A",
                        ErrorMessage = "Cache Store could not be created."
                    };
                    this.EventAggregatorInstance.GetEvent<CacheProviderErrorEvent>().Publish(cacheProviderErrorEventArgs);
                }
            }

            var monitoringMgrCtorParams = new Hashtable { { "cacheProvider", this } };
           // this._sqlMonitor = this.DIContainer.Resolve(typeof(IDependencyMonitoringManager), monitoringMgrCtorParams) as IDependencyMonitoringManager;
            switch (this.Database)
            {
                case Database.SqlServer:
                    this._sqlMonitor = this.DIContainer.Resolve<IDependencyMonitoringManager>("SqlMonitoringManager", monitoringMgrCtorParams);
                    break;
                case Database.Oracle:
                    this._sqlMonitor = this.DIContainer.Resolve<IDependencyMonitoringManager>("OracleMonitoringManager", monitoringMgrCtorParams);
                    break;
            }
            Debug.Assert(this._sqlMonitor != null, "SQLMonitoringManager instance must be set here!");

        }

        #endregion

        #region A D D   O V E R L O A D S

        /// <summary>
        /// Adds an object to the cache.
        /// If you use the Add method and an item with the same name already exists in the cache, 
        /// the method will not replace the item and will not raise an exception.   It will return false.
        /// Uses the default expiration configured in the provider settings.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value">Any .NET object that is Serializable</param>
        /// <returns></returns>
        public override bool Add(string key, object value)
        {
            var returnVal = false;
            var addedValue = this._cacheService.Add(key, value, null, DateTime.UtcNow.AddMilliseconds(this.DefaultExpirationTimeInMilliSeconds), Cache.NoSlidingExpiration, CacheItemPriority.Default, null);
            //if (returnValue != null)
            //    throw new InvalidOperationException("key already exist in cache");

            if (addedValue == null)
                returnVal = true;

            return returnVal;
        }

        public override bool Add(string key, object value, bool useDefaultExpiration)
        {
            if (useDefaultExpiration == false)
                throw new ArgumentException(@"Parameter: useDefaultExpiration must be set to true when using this overload.  Else, use Add(string key, object value, long ttlInMilliSeconds)");

            bool result = this.Add(key, value, this.DefaultExpirationTimeInMilliSeconds);
            return result;
        }

        public override bool Add(string key, object value, long ttlInMilliSeconds)
        {
            this._cacheService.Add(
                                    key,
                                    value,
                                    null,
                                    DateTime.UtcNow.AddMilliseconds(ttlInMilliSeconds),
                                    Cache.NoSlidingExpiration,
                                    CacheItemPriority.Default,
                                    null
                                   );
            return true;
        }

        public override bool Add(string key, object value, TimeSpan timeSpan)
        {
            this._cacheService.Add
                                    (
                                        key, value
                                        ,
                                        null,
                                        DateTime.UtcNow.AddMilliseconds(timeSpan.TotalMilliseconds),
                                        Cache.NoSlidingExpiration,
                                        CacheItemPriority.Default, null
                                    );
            return true;
        }

        #endregion

        #region G E T   O V E R L O A D S

        public override object Get(string key)
        {
            return this._cacheService.Get(key);
        }

        public override T Get<T>(string key)
        {
            var result = (T)this._cacheService.Get(key);

            var message = string.Format("Get Cache with key {0} invoked.", key);
            this.LogWriter.Write(message, this.LoggingCategory, 1, (int)LoggingEvent.CacheGet, TraceEventType.Information, key);

            return result;
        }

        public override IDictionary<string, object> Get(params string[] keys)
        {
            Dictionary<string, object> result = null;
            foreach (var key in keys)
            {
                if (result == null) result = new Dictionary<string, object>();

                var keyValue = this._cacheService.Get(key);
                result.Add(key, keyValue);
            }

            return result;
        }

        #endregion

        #region R E M O V E   O V E R L O A D S

        public override void RemoveAll()
        {
            throw new NotImplementedException();
        }

        public override bool Remove(string key)
        {
            this._cacheService.Remove(key);

            var message = string.Format("Remove Cache with key {0} invoked.", key);
            this.LogWriter.Write(message, this.LoggingCategory, 1, (int)LoggingEvent.CacheRemove, TraceEventType.Information, key);

            return true;
        }

        #endregion

        #region P U T   O V E R L O A D S

        /// <summary>
        /// If an item using the same key is already present in the cache, this call will update the corresponding value.
        /// This overload of the Insert method is inserted with no expiration settings.  Priority is Default
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public override bool Put(string key, object value)
        {
            this._cacheService.Insert(key, value);
            this.WriteLog(key);
            return true;
        }

        public override bool Put(string key, object value, bool useDefaultTimeout)
        {
            if (!useDefaultTimeout)
                throw new InvalidOperationException("parameter [useDefaultTimeout] must be true for this Put() Overload");

            var result = false;
            var timeout = TimeSpan.FromMilliseconds(this.DefaultExpirationTimeInMilliSeconds);
            if (this.Put(key, value, timeout))
                result = true;

            return result;

        }
        public override bool Put(string key, object value, TimeSpan timeout)
        {
            this._cacheService.Insert(key, value, null, DateTime.UtcNow.AddMilliseconds(timeout.TotalMilliseconds), Cache.NoSlidingExpiration);
            this.WriteLog(key);
            return true;
        }

        public override bool Put(string key, object value, DependencyInfo dependencyInfo)
        {
            return PutWithDependency<object>(key, value, dependencyInfo, TimeSpan.Zero, null, null, ThreadOption.UIThread, false);
        }

        public override bool Put(string key, object value, DependencyInfo dependencyInfo, TimeSpan timeout)
        {
            return this.PutWithDependency<object>(key, value, dependencyInfo, timeout, null, null, ThreadOption.UIThread, false);
        }

        protected override bool PutImpl<T>(string key, object value, string regionName, TimeSpan timeout, IEnumerable<string> cacheTags, DependencyInfo dependencyInfo, Action<T> cacheUpdationCallback, T callbackState, ThreadOption threadOption, bool keepSubscriberReferenceAlive)
        {
            return this.InsertOrUpdate(regionName, cacheTags, key, value, dependencyInfo, timeout, cacheUpdationCallback, callbackState, threadOption, keepSubscriberReferenceAlive);
        }

        private bool InsertOrUpdate<T>(string regionName, IEnumerable<string> cacheTags, string key, object value, DependencyInfo dependencyInfo, TimeSpan timeout, Action<T> cacheUpdationCallback, T callbackState, ThreadOption threadOption, bool keepSubscriberReferenceAlive)
        {
            if (!string.IsNullOrEmpty(regionName))
                throw new NotSupportedException("Regions are not supported by this provider");
            if (cacheTags != null && cacheTags.Count() > 0)
                throw new NotSupportedException("Cache Tags are not supported by this provider");

            return this.PutWithDependency<T>(key, value, dependencyInfo, timeout, cacheUpdationCallback, callbackState,
                                             threadOption, keepSubscriberReferenceAlive);
        }

        protected override bool PutImpl<T>(string key, object value, string regionName, bool useDefaultTimeout, IEnumerable<string> cacheTags, DependencyInfo dependencyInfo, Action<T> cacheUpdationCallback, T callbackState, ThreadOption threadOption, bool keepSubscriberReferenceAlive)
        {
            if (useDefaultTimeout)
            {
                var defaultTimeout = TimeSpan.FromMilliseconds(this.DefaultExpirationTimeInMilliSeconds);
                return this.InsertOrUpdate(regionName, cacheTags, key, value, dependencyInfo, defaultTimeout, cacheUpdationCallback, callbackState, threadOption, keepSubscriberReferenceAlive);
            }

            return false;
        }

        #endregion

        #region P R I V A T E   M E T H O D S

        private void WriteLog(string key)
        {
            var message = string.Format("Cache with key {0} added to namedCache {1}", key, this.CacheName);
            this.LogWriter.Write(message, this.LoggingCategory, 1, (int)LoggingEvent.CachePut,
                        TraceEventType.Information, key);
        }

        private bool PutWithDependency<T>(string key, object value, DependencyInfo dependencyInfo, TimeSpan timeout,
                                          Action<T> cacheUpdationCallback, T callbackState, ThreadOption threadOption,
                                          bool keepSubscriberReferenceAlive)
        {
            bool result = false;
            if (dependencyInfo != null)
            {
                switch (dependencyInfo.CacheDependencyType)
                {
                    case CacheDependencyTypes.File:
                        if (dependencyInfo.Files.Count > 0)
                        {
                            var dependency = new CacheDependency(dependencyInfo.Files.ToArray(), DateTime.Now);
                            this._cacheService.Insert(key, value, dependency);
                            this.WriteLog(key);
                            result = true;
                        }
                        else
                        {
                            throw new ArgumentException("For CacheDependencyType File, atleast one file should be added in the Files property");
                        }
                        break;
                    case CacheDependencyTypes.FileNCacheKey:
                        {
                            if (dependencyInfo.Files.Count > 0 || dependencyInfo.CacheKeys.Count > 0)
                            {
                                var dependency = new CacheDependency(dependencyInfo.Files.ToArray(), dependencyInfo.CacheKeys.ToArray());
                                this._cacheService.Insert(key, value, dependency);
                                this.WriteLog(key);
                                result = true;
                            }
                            else
                            {
                                throw new ArgumentException(@"For CacheDependencyType [FileNCacheKey], atleast one file should be added in the [Files] property AND\OR one Cache Key should be added in the [CacheKeys] property");
                            }
                        }
                        break;
                    //case DependencyInfo.CacheDependencyTypes.SQLCacheDependency:
                    //    if (!string.IsNullOrEmpty(dependencyInfo.DBName) && dependencyInfo.IsSQLTable && !string.IsNullOrEmpty(dependencyInfo.SQLTable))
                    //    {
                    //        if (!this._isSqlCacheDependencyMonitoringEnabled)
                    //        {
                    //            SqlCacheDependencyAdmin.EnableNotifications(dependencyInfo.DBConnectionString);
                    //            SqlCacheDependencyAdmin.EnableTableForNotifications(dependencyInfo.DBConnectionString, dependencyInfo.SQLTable);
                    //            this._isSqlCacheDependencyMonitoringEnabled = true;
                    //        }
                    //        else
                    //        {
                    //            string[] tablesEnabledForNotification =
                    //                SqlCacheDependencyAdmin.GetTablesEnabledForNotifications
                    //                                            (dependencyInfo.DBConnectionString);

                    //            var isTableAlreadyRegistered = (from table in tablesEnabledForNotification
                    //                                            let tableRegistered =
                    //                                                    table.Equals(dependencyInfo.SQLTable,
                    //                                                    StringComparison.InvariantCultureIgnoreCase)
                    //                                            select tableRegistered).FirstOrDefault();
                    //            if (!isTableAlreadyRegistered)
                    //                SqlCacheDependencyAdmin.
                    //                    EnableTableForNotifications(dependencyInfo.DBConnectionString,
                    //                                                dependencyInfo.SQLTable);

                    //        }

                    //        var sqlCacheDependency = new SqlCacheDependency(dependencyInfo.DBName, dependencyInfo.SQLTable);
                    //        this._cacheService.Insert(key, value, sqlCacheDependency);
                    //        result = true;
                    //    }
                    //    else if (!string.IsNullOrEmpty(dependencyInfo.DBConnectionString) && !string.IsNullOrEmpty(dependencyInfo.SelectQuery) && dependencyInfo.IsSQLSelectQuery)
                    //    {
                    //        var sqlCommand = new SqlCommand(dependencyInfo.SelectQuery, new SqlConnection(dependencyInfo.DBConnectionString));
                    //        var sqlCacheDependency = new SqlCacheDependency(sqlCommand);
                    //        this._cacheService.Insert(key, value, sqlCacheDependency);
                    //        result = true;
                    //    }
                    //    else
                    //    {
                    //        throw new ArgumentException("Either Properties [DBName], [IsSQLTable=true] and [SQLTable] properties " +
                    //            "OR Properties [DBConnectionString],[SelectQuery] and  [IsSQLSelectQuery=true] must be set for CacheDependencyType [SQLCacheDependency]");
                    //    }
                    //    break;
                    case CacheDependencyTypes.SQLDependency:
                        if (string.IsNullOrEmpty(dependencyInfo.DBConnectionString)
                            || (dependencyInfo.IsSQLSelectQueryBased && dependencyInfo.SelectQueries.Count != 0)
                            || (dependencyInfo.IsSQLTableBased && dependencyInfo.SQLTables.Count != 0)
                            || (dependencyInfo.ISDbCommandBased && dependencyInfo.DbCommands.Count > 0))
                        {
                            ProcessPutWithSqlDependency<T>(key, value, dependencyInfo, timeout, cacheUpdationCallback, callbackState, threadOption, keepSubscriberReferenceAlive);
                            result = true;
                        }
                        else
                        {
                            throw new ArgumentException("Properties [DBConnectionString],[SelectQuery] and  [IsSQLSelectQuery=true] must be set for CacheDependencyType [SQLDependency]");
                        }
                        break;
                    default:
                        throw new NotSupportedException(dependencyInfo.CacheDependencyType.ToString() + " is not supported");
                }
            }

            return result;
        }

        private void ProcessPutWithSqlDependency<T>(string key, object value, DependencyInfo dependencyInfo, TimeSpan timeout,
                                                    Action<T> cacheUpdationCallback, T callbackState, ThreadOption threadOption,
                                                    bool keepSubscriberReferenceAlive)
        {
            var status = this._sqlMonitor.StartMonitoring(dependencyInfo.DBConnectionString);

            if (status != CacheDependencyStatus.MonitoringStarted
                && status != CacheDependencyStatus.AlreadyMonitoring)
            {
                var cacheProviderErrorEventArgs = new CacheProviderErrorEventArgs()
                {
                    Sender = this,
                    CacheKey = key,
                    ErrorMessage = "SQL Monitoring could not be started"
                };
                this.EventAggregatorInstance.GetEvent<CacheProviderErrorEvent>().Publish(cacheProviderErrorEventArgs);
                return;
            }

            if (timeout == TimeSpan.Zero)
                this._cacheService.Insert(key, value);
            else
                this._cacheService.Insert
                    (key,
                     value,
                     null,
                     DateTime.UtcNow.AddMilliseconds(timeout.TotalMilliseconds),
                     Cache.NoSlidingExpiration,
                     CacheItemPriority.Default,
                     ItemRemovedCallback);

            try
            {
                this._sqlMonitor.AddSqlDependencies(key, string.Empty, string.Empty, dependencyInfo, false, cacheUpdationCallback, callbackState, threadOption, keepSubscriberReferenceAlive, TimeSpan.Zero, null);
                this.WriteLog(key);
            }
            catch (Exception)//In case of exception, remove the cache entry.
            {
                this.Remove(key);
                throw;
            }
        }

        private void ItemRemovedCallback(string key, object value, CacheItemRemovedReason reason)
        {
            this._sqlMonitor.RemoveSqlDependency(key, string.Empty, string.Empty, null);
        }

        #endregion

    }
}